<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Mouse Stroke - Help</title>
<link type="image/png" rel="icon" href="icons/icon-16.png"/>
<link type="text/css" rel="stylesheet" href="960.css"/>
<link type="text/css" rel="stylesheet" href="options.css"/>
<script type="text/javascript" src="essentials.js"></script>
<script type="text/javascript" src="actions.js"></script>
<script type="text/javascript" src="stroke.js"></script>
<script type="text/javascript" src="locales/locales.js"></script>
<script type="text/javascript">
/* the id of the selected stroke command */
var _stroke_id = null;

/* stroke event from stroke.js */
var _event;

/* config object */
var _cfg;

/* port to the background page */
var _port = chrome.extension.connect({name: 'stroke.html-config'});

/* distinguish built-in strokes and user defined strokes.
 * user defined strokes' IDs are prefixed with CUS_PFX, there are no conflicts
 * as long as built-in strokes' IDs don't use this prefix */
var iscus = function(id) {
	return !!id.match(new RegExp('^' + CUS_PFX));
}

/* convert id to a readable label */
var id2label = function(id) {
	if (iscus(id))
		return unescape(id.replace(new RegExp('^' + CUS_PFX), ''));
	else
		return id.replace(/\-/g, ' ').replace(/\w+/g, function(s) {
			return s.charAt(0).toUpperCase() + s.substring(1);
		});
}

/* convert label to id, only used by user defined strokes */
var label2id = function(label) {
	return CUS_PFX + escape(label);
}

/* pack stroke sequence into a space separated string */
var pack = function(seq) {
	return (!!seq && seq.join) ? seq.join(' ').trim() : '';
}

/* generate element IDs based on the user defined stroke id */
var cus_label_id = function(id) {
	return id + '-label';
}
var cus_row_id = function(id) {
	return id + '-row';
}

/* construct the config form */
var construct = function(cfg)
{
	var id, loc, outer, inner, box, input, set, unset, cat;

	/* restore options */
	$.get('trigger').value = cfg.trigger;
	$.get('timeout').value = cfg.timeout;
	$.get('minstep').value = cfg.minstep;
	$.get('trail').value = cfg.trail;
	$.get('suppress').checked = !!cfg.suppress;

	/* fill in available languages */
	loc = $.get('locale');
	for (id in _locales) {
		loc.appendChild($.create('option', {
			value: id, textContent: _locales[id]
		}));
	}
	loc.value = cfg.locale;

	/* stroke categories will be flatted for easy referring */
	cfg.strokes = {};

	/* restore built-in stroke settings */
	outer = $.get('built-in-stokes');
	for (id in cfg.categories) {
		outer.appendChild($.create('p', {
			className: 'title',
			textContent: id2label(id)
		}));
		inner = outer.appendChild($.create('div', {
			className: 'group'
		}));

		/* strokes */
		cat = cfg.categories[id];
		for (id in cat) {
			box = inner.appendChild($.create('div', {
				className: 'row clear'
			}));
			box.appendChild($.create('p', {
				className: 'label grid_3 alpha',
				textContent: id2label(id)
			}));
			box = box.appendChild($.create('p', {
				className: 'field grid_5 omega'
			}));
			/* stroke field */
			input = $.create('input', {
				id: id, type: 'text', disabled: true,
				className: 'stroke-field', value: pack(cat[id])
			});
			/* set button */
			set = $.create('button', {
				className: 'set-stroke-btn', 'for': id,
				textContent: 'Set'
			});
			set.addEventListener('click', set_stroke, false);
			/* unset button */
			unset = $.create('button', {
				className: 'unset-stroke-btn', 'for': id,
				textContent: 'Unset'
			});
			unset.addEventListener('click', unset_stroke, false);

			box.appendChild(input);
			box.appendChild(set);
			box.appendChild(unset);

			/* flat categories, all operations on cfg.strokes
			 * should done by reference, so that changes can be
			 * reflected on their counterparts in cfg.categories */
			cfg.strokes[id] = cat[id];
		}
	}

	/* rocker and wheel strokes */
	pxy_construct(cfg);

	/* restore user defined stroke settings */
	box = $.get('custom-strokes');
	for (id in cfg.customs) {
		box.appendChild(cus_construct(id));
	}

	/* bind restore/save button listener */
	$.get('top-restore-btn').addEventListener('click', restore, false);
	$.get('bot-restore-btn').addEventListener('click', restore, false);
	$.get('top-save-btn').addEventListener('click', harvest, false);
	$.get('bot-save-btn').addEventListener('click', harvest, false);
}

var pxy_construct = function(cfg) {
	var proxies = ['rockers-L', 'rockers-R',
		'wheels-U', 'wheels-D', 'wheels-L', 'wheels-R',
		'drags-U', 'drags-D', 'drags-L', 'drags-R'],
		i, id, sel, grp, types, main, sub;
	for (i = 0; i < proxies.length; ++i) {
		sel = $.get(proxies[i]);
		types = sel.id.split('-');
		main = types[0], sub = types[1];

		sel.appendChild($.create('option', {
			value: '', textContent: 'None'
		}));

		if (main == 'drags') {
			/* for drags, only link type strokes are supported */
			for (id in cfg.categories.link) {
				sel.appendChild($.create('option', {
					value: id, textContent: id2label(id)
				}));
			}
		} else {
			/* rockers and wheels support them all */
			/* fill in built-in strokes */
			sel.appendChild(grp = $.create('optgroup', {
				label: 'Built-in Strokes'
			}));
			for (id in cfg.strokes) {
				grp.appendChild($.create('option', {
					value: id, textContent: id2label(id)
				}));
			}

			/* fill in custom strokes */
			sel.appendChild(grp = $.create('optgroup', {
				label: 'User Defined Strokes'
			}));
			for (id in cfg.customs) {
				grp.appendChild($.create('option', {
					value: id, textContent: id2label(id)
				}));
			}
		}

		sel.value = cfg[main][sub];
		sel.addEventListener('change', function(ev) {
			var ids = ev.srcElement.id.split('-');
			_cfg[ids[0]][ids[1]] = ev.srcElement.value;
		}, false);
	}
	if (!WIN) {
		$.hide('windows-exclusive');
	}
}

/* create UI for user defined strokes */
var cus_construct = function(id) {
	var row, box, input, set, unset, remove, stroke = _cfg.customs[id];

	row = $.create('div', { id: cus_row_id(id), className: 'row clear' });
	/* label text */
	row.appendChild($.create('p', {
		id: cus_label_id(id), className: 'label grid_3 alpha',
		textContent: id2label(id)
	}));
	box = row.appendChild($.create('p', {
		className: 'field grid_5 omega'
	}));
	/* stroke field */
	input = $.create('input', {
		id: id, type: 'text', disabled: true,
		className: 'stroke-field', value: pack(stroke)
	});
	/* set button */
	set = $.create('button', {
		className: 'set-stroke-btn', 'for': id, textContent: 'Set'
	});
	set.addEventListener('click', set_stroke, false);
	/* unset button */
	unset = $.create('button', {
		className: 'unset-stroke-btn', 'for': id, textContent: 'Unset'
	});
	unset.addEventListener('click', unset_stroke, false);
	/* remove button */
	remove = $.create('button', {
		className: 'remove-stroke-btn', 'for': id, textContent: 'Remove'
	});
	remove.addEventListener('click', remove_stroke, false);

	box.appendChild(input);
	box.appendChild(set);
	box.appendChild(unset);
	box.appendChild(remove);

	return row;
}

/* save settings */
var harvest = function()
{
	var i, el, customs;

	_cfg.trigger = parseInt($.get('trigger').value);
	_cfg.timeout = parseInt($.get('timeout').value);
	_cfg.minstep = parseInt($.get('minstep').value);
	_cfg.trail = $.get('trail').value;
	_cfg.locale = $.get('locale').value;
	_cfg.suppress = $.get('suppress').checked;

	/* if the label of a user defined stroke was changed, its internal
	 * order within _cfg.customs will also change, so we need to re-sort
	 * _cfg.customs by elements order here */
	for (el = $.get('custom-strokes').getElementsByClassName('stroke-field'),
		i = 0, customs = {};
		i < el.length;
		customs[el[i].id] = _cfg.customs[el[i++].id]);
	_cfg.customs = customs;

	/* save locally */
	_port.postMessage({ type: 'save', data: _cfg });
	$.notify(_('Save complete'), 2);
}

/* restore default settings */
var restore = function()
{
	if (!confirm(_('All settings will be reset, except user defined strokes, proceed?')))
		return;

	_port.postMessage({
		type: 'save',
		data: { customs: _cfg.customs, scripts: _cfg.scripts }
	});
	location.reload(true);
}

/* event handler for setting stroke */
var set_stroke = function(e)
{
	var is_custom, stroke;

	/* set the command id, so the stroke listener knows which stroke
	 * should be set */
	_stroke_id = e.target['for'];
	is_custom = iscus(_stroke_id);
	stroke = is_custom ? _cfg.customs[_stroke_id] : _cfg.strokes[_stroke_id];

	if (is_custom) {
		$.get('custom-label').value = id2label(_stroke_id);
		$.get('custom-script').value = _cfg.scripts[_stroke_id] || '';
		$.show('custom-content');
	}
	/* display the overlay, waiting for input */
	$.get('which-stroke').textContent = id2label(_stroke_id);
	$.get('old-stroke').textContent = pack(stroke);
	$.get('new-stroke').textContent = '';
	$.show('overlay');
}

/* event handler for unsetting stroke */
var unset_stroke = function(e) {
	var k = e.target['for'],
		s = iscus(k) ? _cfg.customs[k] : _cfg.strokes[k];
	/* unset the stroke */
	$.get(k).value = '';
	/* empty the stroke array */
	s.splice(0, s.length);
}

/* event handler for remove user defined stroke */
var remove_stroke = function(e) {
	var k = e.target['for'], tr;
	if (confirm(_('Remove user defined stroke: "%"?', id2label(k)))) {
		tr = $.get(cus_row_id(k));
		tr.parentNode.removeChild(tr);
		delete _cfg.customs[k];
		delete _cfg.scripts[k];
	}
}

/* event handler for add new user defined stroke */
var add_stroke = function(e) {
	var id, i = 1, label = 'New User Defined Stroke ';

	do { id = label2id(label + i++) }
	while (!!_cfg.customs[id]);
	/* push a special new stroke to customs */
	_cfg.customs[id] = [];
	_cfg.customs[id].neo = true;
	/* tell a lie */
	e.target['for'] = id;
	set_stroke(e);
}

/* event handler for the confirm button */
var confirm_change = function(e) {
	var k, tr, label, script, newstk, is_custom = iscus(_stroke_id),
		bucket = is_custom ? _cfg.customs : _cfg.strokes,
		stroke = bucket[_stroke_id],
		id = _stroke_id;

	/* label and script */
	if (is_custom) {
		label = $.get('custom-label').value.trim();
		script = $.get('custom-script').value;
		if (!label || !script.trim()) {
			$.notify(_('Both [Label] and [Script] can not be empty'), 4);
			return;
		}
		/*if (!evaluate(script)) return;*/

		/* id does not equal to _stroke_id if label is changed */
		id = label2id(label);
		/* notify conflicts */
		if (_stroke_id != id && !!bucket[id] && !confirm(_('"%" already exists, overwrite?', label)))
			return;
	}

	/* update stroke setting */
	if (!!_event && !!_event.stroke) {
		newstk = pack(_event.stroke);
		/* check duplicates */
		for (k in bucket) {
			if (id != k && newstk == pack(bucket[k])) {
				if (confirm(_('[%] has been assigned to "%", overwrite?', newstk, id2label(k)))) {
					/* clear conflicted settings */
					$.get(k).value = '';
					bucket[k].splice(0, bucket[k].length);
					break;
				} else {
					return;
				}
			}
		}
		/* operate on the array reference, replace array elements
		 * with elements of _event.stroke */
		stroke.splice.apply(stroke, [0, stroke.length].concat(_event.stroke));
	}

	/* handle label change, update page elements */
	if (_stroke_id != id || stroke.neo) {
		/* a new stroke, or label changed */
		bucket[id] = stroke;
		tr = $.get(cus_row_id(_stroke_id));
		/* this row exists when the user confirmed overwriting an
		 * existing label */
		if (!$.get(cus_row_id(id)))
			$.get('custom-strokes').insertBefore(cus_construct(id), tr);
		if (_stroke_id != id) {
			delete bucket[_stroke_id];
			delete _cfg.scripts[_stroke_id];
			/* tr is null if this is a new stroke */
			if (!!tr) tr.parentNode.removeChild(tr);
		}
		/* from now on _stroke_id is replaced by new id, later
		 * references to _stroke_id should be pointed to the new id */
		_stroke_id = id;
	}
	if (!!newstk || stroke.neo) $.get(id).value = newstk || pack(stroke);

	if (is_custom) _cfg.scripts[id] = script;
	if (stroke.neo) delete stroke.neo;

	cancel_change(e);
}

/* event handler for the cancel button */
var cancel_change = function(e) {
	/* cancel setting stroke */
	if (!!_cfg.customs[_stroke_id] && _cfg.customs[_stroke_id].neo) {
		/* a new stroke is cancelled */
		delete _cfg.customs[_stroke_id];
		delete _cfg.scripts[_stroke_id];
	}
	$.hide('overlay');
	$.hide('custom-content');
	_stroke_id = null;
	_event = null;
}

/* evaluate scripts of user defined strokes */
var evaluate = function(script) {
	var ex, msg, line, sbox = $.get('sandbox').contentWindow;
	if (true !== (ex = sbox.sandbox.call({}, {}, script))) {
		/* extract compiler messages */
		ex = ex.stack.split("\n");
		line = ex[1].replace(/.+actions\.js:\d+:(\d+).+$/, '$1');
		msg = "The compiler reported a script error.\n\n";
		msg += ex[0]/* + "\nAt line: " + (line ? line : '<unknown>')*/;
		msg += "\n\nProceed with errors?";

		return confirm(msg);
	}
	return true;
}

/* stroke.js will fire an pull-config command to stroke.html at start up,
 * we listen to the response too */
chrome.extension.onRequest.addListener(function(msg, sender, resp) {
	/* apply only once */
	if (!_cfg && 'update-config' == msg.type) {
		construct(_cfg = msg.data);
	}
	try { resp({}) } catch (e) {}
});

window.addEventListener('load', function() {
	var sbox;
	$.get('confirm-btn').addEventListener('click', confirm_change, false);
	$.get('cancel-btn').addEventListener('click', cancel_change, false);
	$.get('add-custom-btn').addEventListener('click', add_stroke, false);
	/* prepare the sandbox for evaluating user script
	sbox = $.get('sandbox').contentWindow;
	sbox.document.head.appendChild($.create('script', {
		type: 'text/javascript', src: 'actions.js'
	}));
	sbox.parent = sbox;
	sbox.alert = sbox.confirm = function() {}*/
});

/* listen to the stroke detecting script */
document.addEventListener('set-stroke-event', function(ev) {
	if (!!_stroke_id && !!_cfg[iscus(_stroke_id) ? 'customs' : 'strokes'][_stroke_id]) {
		/* valid _stroke_id */
		$.get('new-stroke').textContent = pack(ev.event.stroke);
		/* cache event */
		_event = ev.event;
	}
}, false);
</script>
<body class="container_16">

<div id="overlay" class="overlay hidden">
 <h1 id="which-stroke"></h1>
 <p>Previous<br/>[ <span id="old-stroke" class="old-stroke"></span> ]</p>
 <p>Current<br/>[ <span id="new-stroke" class="new-stroke"></span> ]</p>
 <div class="form">
  <div id="custom-content" class="custom-content hidden">
   <p><em>Label</em> *<br/>
    <input id="custom-label" class="custom-label" type="text" maxlength="64"/>
   </p>
   <p><em>Script</em> *<br/>
    <textarea id="custom-script" class="custom-script" spellcheck="false"></textarea>
   </p>
  </div>
 </div>
 <p>
  <button id="confirm-btn">Confirm</button>
  <button id="cancel-btn">Cancel</button>
 </p>
</div>

<!--iframe id="sandbox" class="hidden"></iframe-->

<div id="header" class="header grid_16">
 <h1><a href="http://code.google.com/p/chrome-mouse-stroke/">Mouse Stroke</a></h1>

 <p>
  This is a Google Chrome extension which enables
  <a href="http://en.wikipedia.org/wiki/Mouse_gesture">mouse strokes</a>
  (also known as mouse gestures) in the browser. With this extension, you can
  perform common tasks by simply moving your mouse.
 </p>

 <ol class="eye-catcher">
  <li><strong>New in 1.8</strong>:
   <em>Rocker Strokes</em>, <em>Mouse Wheel Strokes</em> and <em>Drag'n'Drop
   Strokes</em> are added in this version.
  </li>
  <li><strong>Linux support</strong>:
   Please use the middle mouse button as the trigger key, it works on my
   Debian box. Be aware of some
   <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/KnownIssues#Linux_and_Mac_support">known issues</a>.
  </li>
  <li><strong>Help needed</strong>:
   Mouse Stroke needs
   <a href="http://code.google.com/p/chrome-mouse-stroke/issues/detail?id=15">an icon design</a>.
  </li>
 </ol>
</div>

<div class="submit grid_16">
 <button id="top-restore-btn">Restore to default</button>
 <button id="top-save-btn">Save</button>
</div>

<div id="options" class="grid_16">
 <p class="title">Options</p>
 <div class="group">
  <div class="row clear hidden">
   <p class="label grid_8 push_5 alpha">Language</p>
   <p class="field grid_8 omega">
    <select id="locale">
    </select>
   </p>
  </div>

  <div class="row clear">
   <p class="label grid_8 push_5 alpha">Stroke Trigger</p>
   <p class="field grid_8 omega">
    <select id="trigger">
     <!--option value="0">Left Mouse Button</option-->
     <option value="1">Middle Mouse Button (Linux and Mac)</option>
     <option value="2">Right Mouse Button (Windows)</option>
    </select>
   </p>
  </div>

  <div class="row clear">
   <p class="label grid_8 push_5 alpha">Stroke Timeout (Milliseconds)</p>
   <p class="field grid_8 omega">
    <input id="timeout" type="text"/>
    <em><sub>Try >1000 if you feel difficult to perform strokes</sub></em>
   </p>
  </div>

  <div class="row clear">
   <p class="label grid_8 push_5 alpha">Minimal Step (Pixels)</p>
   <p class="field grid_8 omega"><input id="minstep" type="text"/></p>
  </div>

  <div class="row clear">
   <p class="label grid_8 push_5 alpha">Draw Trails</p>
   <p class="field grid_8 omega">
    <select id="trail">
     <option value="">None</option>
     <option value="black" style="color:black">Black</option>
     <option value="red" style="color:red">Red</option>
     <option value="green" style="color:green">Green</option>
     <option value="yellow" style="color:yellow">Yellow</option>
     <option value="blue" style="color:blue">Blue</option>
     <option value="magenta" style="color:magenta">Magenta</option>
     <option value="cyan" style="color:cyan">Cyan</option>
    </select>
   </p>
  </div>

  <div class="row clear">
   <p class="label grid_8 push_5 alpha">Alt key suppresses strokes</p>
   <p class="field grid_8 omega"><input id="suppress" type="checkbox"/></p>
  </div>
 </div>
</div>

<div id="built-in-stokes" class="grid_8"></div>

<div class="grid_8">
 <div id="windows-exclusive">
  <p class="title">Rocker Strokes</p>
  <div id="rocker-strokes" class="group">
   <div class="row clear">
    <p class="label grid_3 alpha">Left click while holding the right button</p>
    <p class="field grid_5 omega"><select id="rockers-L"></select></p>
   </div>

   <div class="row clear">
    <p class="label grid_3 alpha">Right click while holding the left button</p>
    <p class="field grid_5 omega"><select id="rockers-R"></select></p>
   </div>
  </div>

  <p class="title">Mouse Wheel Strokes (Scroll while holding the right button)</p>
  <div id="wheel-strokes" class="group">
   <div class="row clear">
    <p class="label grid_3 alpha">Mouse wheel up</p>
    <p class="field grid_5 omega"><select id="wheels-U"></select></p>
   </div>

   <div class="row clear">
    <p class="label grid_3 alpha">Mouse wheel down</p>
    <p class="field grid_5 omega"><select id="wheels-D"></select></p>
   </div>

   <div class="row clear">
    <p class="label grid_3 alpha">Mouse wheel left</p>
    <p class="field grid_5 omega"><select id="wheels-L"></select></p>
   </div>

   <div class="row clear">
    <p class="label grid_3 alpha">Mouse wheel right</p>
    <p class="field grid_5 omega"><select id="wheels-R"></select></p>
   </div>
  </div>
 </div>

 <p class="title">Drag'n'Drop Strokes (Drag links by left mouse key)</p>
 <div id="drag-strokes" class="group">
  <div class="row clear">
   <p class="label grid_3 alpha">Drop up</p>
   <p class="field grid_5 omega"><select id="drags-U"></select></p>
  </div>

  <div class="row clear">
   <p class="label grid_3 alpha">Drop down</p>
   <p class="field grid_5 omega"><select id="drags-D"></select></p>
  </div>

  <div class="row clear">
   <p class="label grid_3 alpha">Drop left</p>
   <p class="field grid_5 omega"><select id="drags-L"></select></p>
  </div>

  <div class="row clear">
   <p class="label grid_3 alpha">Drop right</p>
   <p class="field grid_5 omega"><select id="drags-R"></select></p>
  </div>
 </div>

 <p class="title">
  User Defined Strokes (Ctrl key + drawing strokes)
  <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/FAQs#How_to_create_my_own_strokes?">Guide</a> |
  <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/UserDefinedStrokeGallery">Share</a>
 </p>
 <div id="custom-strokes" class="group"></div>

 <p><button id="add-custom-btn">New Stroke</button></p>
</div>

<div class="submit grid_16">
 <button id="bot-restore-btn">Restore to default</button>
 <button id="bot-save-btn">Save</button>
</div>

<div id="footer" class="footer grid_16">
 <hr/>
 <p>
  <a href="http://code.google.com/p/chrome-mouse-stroke/">Home</a> |
  <a href="http://code.google.com/p/chrome-mouse-stroke/issues/list">Support</a> |
  <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/">Wiki</a> |
  <a href="http://code.google.com/u/vigacmoe/">Author</a>
 </p>
</div>

</body>
</html>
